// RuleSet base class
// needs to cope with nested CSS files
// doesn't do untagged nodes optimisation

import styleparser from "./Style.js";

styleparser.RuleSet = function () {};

styleparser.RuleSet.prototype = {
  choosers: [], // list of StyleChoosers

  getStyles: function (entity, tags, zoom) {
    // summary:		Find the styles for a given entity.
    var sl = new styleparser.StyleList();
    for (var i in this.choosers) {
      this.choosers[i].updateStyles(entity, tags, sl, zoom);
    }
    return sl; // styleparser.StyleList
  },

  /*loadFromCSS: function(url, callback) {
        // summary:		Load a MapCSS file from a URL, then throw it at the parser when it's loaded.
        this.callback = callback;
        $.ajax({
            url: url,
            success: _.bind(this.parseCSS, this)
        });
    },*/

  parseCSS: function (css) {
    // summary:		Parse a CSS document into a set of StyleChoosers.
    var previous = 0; // what was the previous CSS word?
    var sc = new styleparser.StyleChooser(); // currently being assembled
    this.choosers = [];
    css = css.replace(/[\r\n]/g, ""); // strip linebreaks because JavaScript doesn't have the /s modifier

    var o = {};
    while (css.length > 0) {
      // CSS comment
      if ((o = this.COMMENT.exec(css))) {
        css = css.replace(this.COMMENT, "");

        // Whitespace (probably only at beginning of file)
      } else if ((o = this.WHITESPACE.exec(css))) {
        css = css.replace(this.WHITESPACE, "");

        // Class - .motorway, .builtup, :hover
      } else if ((o = this.CLASS.exec(css))) {
        if (previous == this.oDECLARATION) {
          this.saveChooser(sc);
          sc = new styleparser.StyleChooser();
        }

        css = css.replace(this.CLASS, "");
        sc.currentChain().addConditionToLast(
          new styleparser.Condition().init("set", o[1])
        );
        previous = this.oCONDITION;

        // Not class - !.motorway, !.builtup, !:hover
      } else if ((o = this.NOT_CLASS.exec(css))) {
        if (previous == this.oDECLARATION) {
          this.saveChooser(sc);
          sc = new styleparser.StyleChooser();
        }

        css = css.replace(this.NOT_CLASS, "");
        sc.currentChain().addConditionToLast(
          new styleparser.Condition().init("unset", o[1])
        );
        previous = this.oCONDITION;

        // Zoom
      } else if ((o = this.ZOOM.exec(css))) {
        if (previous != this.oOBJECT && previous != this.oCONDITION) {
          sc.currentChain().addRule();
        }

        css = css.replace(this.ZOOM, "");
        var z = this.parseZoom(o[1]);
        sc.currentChain().addZoomToLast(z[0], z[1]);
        sc.zoomSpecific = true;
        previous = this.oZOOM;

        // Grouping - just a comma
      } else if ((o = this.GROUP.exec(css))) {
        css = css.replace(this.GROUP, "");
        sc.newRuleChain();
        previous = this.oGROUP;

        // Condition - [highway=primary]
      } else if ((o = this.CONDITION.exec(css))) {
        if (previous == this.oDECLARATION) {
          this.saveChooser(sc);
          sc = new styleparser.StyleChooser();
        }
        if (
          previous != this.oOBJECT &&
          previous != this.oZOOM &&
          previous != this.oCONDITION
        ) {
          sc.currentChain().addRule();
        }
        css = css.replace(this.CONDITION, "");
        sc.currentChain().addConditionToLast(this.parseCondition(o[1]));
        previous = this.oCONDITION;

        // Object - way, node, relation
      } else if ((o = this.OBJECT.exec(css))) {
        // TODO: raise error if object is none of node|way|relation|line|area|canvas|* ?
        if (previous == this.oDECLARATION) {
          this.saveChooser(sc);
          sc = new styleparser.StyleChooser();
        }

        css = css.replace(this.OBJECT, "");
        sc.currentChain().addRule(o[1]);
        previous = this.oOBJECT;

        // Subpart - ::centreline
      } else if ((o = this.SUBPART.exec(css))) {
        if (previous == this.oDECLARATION) {
          this.saveChooser(sc);
          sc = new styleparser.StyleChooser();
        }
        css = css.replace(this.SUBPART, "");
        sc.currentChain().setSubpart(o[1]);
        previous = this.oSUBPART;

        // Declaration - {...}
      } else if ((o = this.DECLARATION.exec(css))) {
        css = css.replace(this.DECLARATION, "");
        sc.addStyles(this.parseDeclaration(o[1]));
        previous = this.oDECLARATION;

        // Unknown pattern
      } else if ((o = this.UNKNOWN.exec(css))) {
        css = css.replace(this.UNKNOWN, "");
        // TODO: own error class
        throw new Error(
          'Error while parsing MapCSS at "' +
            o[1] +
            (css.length > 38 ? css.substr(0, 36) + "..." : css) +
            '"'
        );
        // console.log("unknown: "+o[1]);
      } else {
        // console.log("choked on "+css);
        throw new Error("MapCSS parsing choked on " + css);
        return;
      }
    }
    if (previous == this.oDECLARATION) {
      this.saveChooser(sc);
      sc = new styleparser.StyleChooser();
    }
    if (this.callback) {
      this.callback();
    }
  },

  saveChooser: function (sc) {
    this.choosers.push(sc);
  },

  parseDeclaration: function (s) {
    var styles = [];
    var t = {};
    var o = {};
    var k, v;

    // Create styles
    var ss = new styleparser.ShapeStyle();
    var ps = new styleparser.PointStyle();
    var ts = new styleparser.TextStyle();
    var hs = new styleparser.ShieldStyle();
    var xs = new styleparser.InstructionStyle();

    var r = s.split(";");
    var isEval = {};
    for (var i in r) {
      var a = r[i];
      if ((o = this.ASSIGNMENT_EVAL.exec(a))) {
        k = o[1].replace(this.DASH, "_");
        t[k] = o[2];
        isEval[k] = true;
      } else if ((o = this.ASSIGNMENT.exec(a))) {
        k = o[1].replace(this.DASH, "_");
        t[k] = o[2];
      } else if ((o = this.SET_TAG_EVAL.exec(a))) {
      } else if ((o = this.SET_TAG.exec(a))) {
        // xs.addSetTag(o[1],this.saveEval(o[2]));
        xs.addSetTag(o[1], o[2]);
      } else if ((o = this.SET_TAG_TRUE.exec(a))) {
        xs.addSetTag(o[1], true);
      } else if ((o = this.EXIT.exec(a))) {
        xs.setPropertyFromString("breaker", true);
      }
    }

    /*// Find sublayer
        // TODO: Why the renaming z_index -> sublayer?
        //       This hardcoded variable isn't used anywhere.
        //       Looks like this was replaced by the "subparts"...
        var sub=5;
        if (t['z_index']) { sub=Number(t['z_index']); delete t['z_index']; }
        ss.sublayer=ps.sublayer=ts.sublayer=hs.sublayer=sub;
        xs.sublayer=10;*/

    /*// Find "interactive" property - it's true unless explicitly set false
        var inter=true;
        if (t['interactive']) { inter=t['interactive'].match(this.FALSE) ? false : true; delete t['interactive']; }
        ss.interactive=ps.interactive=ts.interactive=hs.interactive=xs.interactive=inter;*/

    // Munge special values
    // (we should stop doing this and do it in the style instead)
    // TODO: font_bold??? wtf? -> support propper MapCSS properites!
    if (t["font_weight"]) {
      t["font_bold"] = t["font_weight"].match(this.BOLD) ? true : false;
      delete t["font_weight"];
    }
    if (t["font_style"]) {
      t["font_italic"] = t["font_style"].match(this.ITALIC) ? true : false;
      delete t["font_style"];
    }
    if (t["text_decoration"]) {
      t["font_underline"] = t["text_decoration"].match(this.UNDERLINE)
        ? true
        : false;
      delete t["text_decoration"];
    }
    if (t["text_position"]) {
      t["text_center"] = t["text_position"].match(this.CENTER) ? true : false;
      delete t["text_position"];
    }
    if (t["text_transform"]) {
      if (t["text_transform"].match(this.CAPS)) {
        t["font_caps"] = true;
      } else {
        t["font_caps"] = false;
      }
      delete t["text_transform"];
    }

    // Assign each property to the appropriate style
    for (a in t) {
      // Parse properties
      v = t[a];

      // Set in styles
      if (ss.has(a)) {
        ss.setPropertyFromString(a, v, isEval[a]);
      } else if (ps.has(a)) {
        ps.setPropertyFromString(a, v, isEval[a]);
      } else if (ts.has(a)) {
        ts.setPropertyFromString(a, v, isEval[a]);
      } else if (hs.has(a)) {
        hs.setPropertyFromString(a, v, isEval[a]);
      } else {
        // console.log(a+" not found");
      }
    }

    // Add each style to list
    if (ss.edited) {
      styles.push(ss);
    }
    if (ps.edited) {
      styles.push(ps);
    }
    if (ts.edited) {
      styles.push(ts);
    }
    if (hs.edited) {
      styles.push(hs);
    }
    if (xs.edited) {
      styles.push(xs);
    }
    return styles;
  },

  parseZoom: function (s) {
    var o = {};
    var maxscale = 999; // TODO: hardcoded
    var minscale = -999; // TODO: hardcoded
    if ((o = this.ZOOM_MINMAX.exec(s))) {
      return [o[1], o[2]];
    } else if ((o = this.ZOOM_MIN.exec(s))) {
      return [o[1], maxscale];
    } else if ((o = this.ZOOM_MAX.exec(s))) {
      return [minscale, o[1]];
    } else if ((o = this.ZOOM_SINGLE.exec(s))) {
      return [o[1], o[1]];
    }
    return null;
  },

  parseCondition: function (s) {
    var o = {};
    if ((o = this.CONDITION_TRUE.exec(s))) {
      return new styleparser.Condition().init("true", o[1]);
    } else if ((o = this.CONDITION_FALSE.exec(s))) {
      return new styleparser.Condition().init("false", o[1]);
    } else if ((o = this.CONDITION_SET.exec(s))) {
      return new styleparser.Condition().init("set", o[1]);
    } else if ((o = this.CONDITION_UNSET.exec(s))) {
      return new styleparser.Condition().init("unset", o[1]);
    } else if ((o = this.CONDITION_NE.exec(s))) {
      return new styleparser.Condition().init("ne", o[1], o[2]);
    } else if ((o = this.CONDITION_GT.exec(s))) {
      return new styleparser.Condition().init(">", o[1], o[2]);
    } else if ((o = this.CONDITION_GE.exec(s))) {
      return new styleparser.Condition().init(">=", o[1], o[2]);
    } else if ((o = this.CONDITION_LT.exec(s))) {
      return new styleparser.Condition().init("<", o[1], o[2]);
    } else if ((o = this.CONDITION_LE.exec(s))) {
      return new styleparser.Condition().init("<=", o[1], o[2]);
    } else if ((o = this.CONDITION_REGEX.exec(s))) {
      return new styleparser.Condition().init("regex", o[1], o[2]);
    } else if ((o = this.CONDITION_EQ.exec(s))) {
      return new styleparser.Condition().init("eq", o[1], o[2]);
    }
    return null;
  },

  parseCSSColor: function (colorStr) {
    // todo: this should be done at user (=style consumer) side (if necessary).
    // -> move to a more appropriate location
    colorStr = colorStr.toLowerCase();
    if (this.CSSCOLORS[colorStr]) {
      return this.CSSCOLORS[colorStr];
    } else {
      var match = this.HEX.exec(colorStr);
      if (match) {
        if (match[1].length == 3) {
          // repeat digits. #abc => 0xaabbcc
          return Number(
            "0x" +
              match[1].charAt(0) +
              match[1].charAt(0) +
              match[1].charAt(1) +
              match[1].charAt(1) +
              match[1].charAt(2) +
              match[1].charAt(2)
          );
        } else if (match[1].length == 6) {
          return Number("0x" + match[1]);
        } else {
          return Number("0x000000"); //as good as any
        }
      }
    }
    return 0;
  },

  // Regular expression tests and other constants

  WHITESPACE: /^\s+/,
  COMMENT: /\/\*.+?\*\/\s*/,
  CLASS: /^([\.:]\w+)\s*/,
  NOT_CLASS: /^!([\.:]\w+)\s*/,
  ZOOM: /^\|\s*z([\d\-]+)\s*/i,
  GROUP: /^,\s*/i,
  CONDITION: /^\[(.+?)\]\s*/,
  OBJECT: /^(\w+)\s*/, // TODO: match also "*" (and handle the wildcard appropriately)
  DECLARATION: /^\{(.+?)\}\s*/,
  SUBPART: /^::(\w+)\s*/,
  UNKNOWN: /^(\S+)\s*/,

  ZOOM_MINMAX: /^(\d+)\-(\d+)$/,
  ZOOM_MIN: /^(\d+)\-$/,
  ZOOM_MAX: /^\-(\d+)$/,
  ZOOM_SINGLE: /^(\d+)$/,

  CONDITION_TRUE: /^\s*([:@\w]+)\s*=\s*yes\s*$/i,
  CONDITION_FALSE: /^\s*([:@\w]+)\s*=\s*no\s*$/i,
  CONDITION_SET: /^\s*([:@\w]+)\s*$/,
  CONDITION_UNSET: /^\s*!([:@\w]+)\s*$/,
  CONDITION_EQ: /^\s*([:@\w]+)\s*=\s*(.+)\s*$/,
  CONDITION_NE: /^\s*([:@\w]+)\s*!=\s*(.+)\s*$/,
  CONDITION_GT: /^\s*([:@\w]+)\s*>\s*(.+)\s*$/,
  CONDITION_GE: /^\s*([:@\w]+)\s*>=\s*(.+)\s*$/,
  CONDITION_LT: /^\s*([:@\w]+)\s*<\s*(.+)\s*$/,
  CONDITION_LE: /^\s*([:@\w]+)\s*<=\s*(.+)\s*$/,
  CONDITION_REGEX: /^\s*([:@\w]+)\s*=~\/\s*(.+)\/\s*$/,

  ASSIGNMENT_EVAL: /^\s*(\S+)\s*\:\s*eval\s*\(\s*['"](.+?)['"]\s*\)\s*$/i, // TODO: match only two matching quotes
  ASSIGNMENT: /^\s*(\S+)\s*\:\s*(.+?)\s*$/,
  SET_TAG_EVAL: /^\s*set\s+(\S+)\s*=\s*eval\s*\(\s*'(.+?)'\s*\)\s*$/i,
  SET_TAG: /^\s*set\s+(\S+)\s*=\s*(.+?)\s*$/i,
  SET_TAG_TRUE: /^\s*set\s+(\S+)\s*$/i,
  EXIT: /^\s*exit\s*$/i,

  oZOOM: 2,
  oGROUP: 3,
  oCONDITION: 4,
  oOBJECT: 5,
  oDECLARATION: 6,
  oSUBPART: 7,

  DASH: /\-/g,
  COLOR: /color$/,
  BOLD: /^bold$/i,
  ITALIC: /^italic|oblique$/i,
  UNDERLINE: /^underline$/i,
  CAPS: /^uppercase$/i,
  CENTER: /^center$/i,
  FALSE: /^(no|false|0)$/i,

  HEX: /^#([0-9a-f]+)$/i,

  CSSCOLORS: {
    aliceblue: 0xf0f8ff,
    antiquewhite: 0xfaebd7,
    aqua: 0x00ffff,
    aquamarine: 0x7fffd4,
    azure: 0xf0ffff,
    beige: 0xf5f5dc,
    bisque: 0xffe4c4,
    black: 0x000000,
    blanchedalmond: 0xffebcd,
    blue: 0x0000ff,
    blueviolet: 0x8a2be2,
    brown: 0xa52a2a,
    burlywood: 0xdeb887,
    cadetblue: 0x5f9ea0,
    chartreuse: 0x7fff00,
    chocolate: 0xd2691e,
    coral: 0xff7f50,
    cornflowerblue: 0x6495ed,
    cornsilk: 0xfff8dc,
    crimson: 0xdc143c,
    cyan: 0x00ffff,
    darkblue: 0x00008b,
    darkcyan: 0x008b8b,
    darkgoldenrod: 0xb8860b,
    darkgray: 0xa9a9a9,
    darkgreen: 0x006400,
    darkkhaki: 0xbdb76b,
    darkmagenta: 0x8b008b,
    darkolivegreen: 0x556b2f,
    darkorange: 0xff8c00,
    darkorchid: 0x9932cc,
    darkred: 0x8b0000,
    darksalmon: 0xe9967a,
    darkseagreen: 0x8fbc8f,
    darkslateblue: 0x483d8b,
    darkslategray: 0x2f4f4f,
    darkturquoise: 0x00ced1,
    darkviolet: 0x9400d3,
    deeppink: 0xff1493,
    deepskyblue: 0x00bfff,
    dimgray: 0x696969,
    dodgerblue: 0x1e90ff,
    firebrick: 0xb22222,
    floralwhite: 0xfffaf0,
    forestgreen: 0x228b22,
    fuchsia: 0xff00ff,
    gainsboro: 0xdcdcdc,
    ghostwhite: 0xf8f8ff,
    gold: 0xffd700,
    goldenrod: 0xdaa520,
    gray: 0x808080,
    green: 0x008000,
    greenyellow: 0xadff2f,
    honeydew: 0xf0fff0,
    hotpink: 0xff69b4,
    indianred: 0xcd5c5c,
    indigo: 0x4b0082,
    ivory: 0xfffff0,
    khaki: 0xf0e68c,
    lavender: 0xe6e6fa,
    lavenderblush: 0xfff0f5,
    lawngreen: 0x7cfc00,
    lemonchiffon: 0xfffacd,
    lightblue: 0xadd8e6,
    lightcoral: 0xf08080,
    lightcyan: 0xe0ffff,
    lightgoldenrodyellow: 0xfafad2,
    lightgrey: 0xd3d3d3,
    lightgreen: 0x90ee90,
    lightpink: 0xffb6c1,
    lightsalmon: 0xffa07a,
    lightseagreen: 0x20b2aa,
    lightskyblue: 0x87cefa,
    lightslategray: 0x778899,
    lightsteelblue: 0xb0c4de,
    lightyellow: 0xffffe0,
    lime: 0x00ff00,
    limegreen: 0x32cd32,
    linen: 0xfaf0e6,
    magenta: 0xff00ff,
    maroon: 0x800000,
    mediumaquamarine: 0x66cdaa,
    mediumblue: 0x0000cd,
    mediumorchid: 0xba55d3,
    mediumpurple: 0x9370d8,
    mediumseagreen: 0x3cb371,
    mediumslateblue: 0x7b68ee,
    mediumspringgreen: 0x00fa9a,
    mediumturquoise: 0x48d1cc,
    mediumvioletred: 0xc71585,
    midnightblue: 0x191970,
    mintcream: 0xf5fffa,
    mistyrose: 0xffe4e1,
    moccasin: 0xffe4b5,
    navajowhite: 0xffdead,
    navy: 0x000080,
    oldlace: 0xfdf5e6,
    olive: 0x808000,
    olivedrab: 0x6b8e23,
    orange: 0xffa500,
    orangered: 0xff4500,
    orchid: 0xda70d6,
    palegoldenrod: 0xeee8aa,
    palegreen: 0x98fb98,
    paleturquoise: 0xafeeee,
    palevioletred: 0xd87093,
    papayawhip: 0xffefd5,
    peachpuff: 0xffdab9,
    peru: 0xcd853f,
    pink: 0xffc0cb,
    plum: 0xdda0dd,
    powderblue: 0xb0e0e6,
    purple: 0x800080,
    red: 0xff0000,
    rosybrown: 0xbc8f8f,
    royalblue: 0x4169e1,
    saddlebrown: 0x8b4513,
    salmon: 0xfa8072,
    sandybrown: 0xf4a460,
    seagreen: 0x2e8b57,
    seashell: 0xfff5ee,
    sienna: 0xa0522d,
    silver: 0xc0c0c0,
    skyblue: 0x87ceeb,
    slateblue: 0x6a5acd,
    slategray: 0x708090,
    snow: 0xfffafa,
    springgreen: 0x00ff7f,
    steelblue: 0x4682b4,
    tan: 0xd2b48c,
    teal: 0x008080,
    thistle: 0xd8bfd8,
    tomato: 0xff6347,
    turquoise: 0x40e0d0,
    violet: 0xee82ee,
    wheat: 0xf5deb3,
    white: 0xffffff,
    whitesmoke: 0xf5f5f5,
    yellow: 0xffff00,
    yellowgreen: 0x9acd32
  }
};
